Popular Searches
Popular Course Categories
Popular Courses

Designing Reusable Object-Oriented Class Structures

Designing Reusable Object-Oriented Class Structures

Abstraction, Interfaces, and Object Relationships

Designing Reusable Object-Oriented Class Structures in Java

Course: Core Java Training

Topic: Designing Reusable Object-Oriented Class Structures

Course Link: Core Java Training Course

Demo Link: Register for a Course Demo

Website: JustAcademy


1. Introduction to Reusable Object-Oriented Class Structures

Designing reusable object-oriented class structures means creating Java classes that can be used in different parts of an application or in multiple applications without repeatedly rewriting the same logic. A reusable class has a clear responsibility, protects its data, exposes appropriate methods, and works effectively with other classes.

Object-oriented programming (OOP) organizes software around classes and objects. A class defines data and behavior, while an object is an instance created from that class. Concepts such as encapsulation, abstraction, inheritance, and polymorphism help developers organize code into understandable and reusable components.

For example, an e-commerce application might contain classes such as Product, Customer, ShoppingCart, and Order. Each class manages a specific responsibility and can cooperate with the others to complete a business operation.

A good reusable design avoids unnecessary duplication, limits dependencies between classes, and makes future changes easier. Java's object-oriented programming learning resources provide additional information about classes, objects, inheritance, and interfaces.

2. Learning Objectives

After studying this topic, you should be able to:

  • Understand reusable class structures in Java.
  • Identify the responsibilities of classes and objects.
  • Apply encapsulation and abstraction to improve class design.
  • Use inheritance and polymorphism when appropriate.
  • Understand composition, aggregation, and association.
  • Design reusable interfaces and abstract classes.
  • Reduce code duplication and unnecessary dependencies.
  • Apply basic SOLID principles to object-oriented design.
  • Organize related classes using Java packages.
  • Build a practical reusable class structure for a business application.

3. What Is a Reusable Class?

A reusable class is a class designed to perform a well-defined responsibility so that other parts of a program can use it without copying its implementation.

For example, a DiscountCalculator class could calculate a discount for different products and orders. Instead of repeating the discount formula throughout the application, developers can call a method from the same class wherever the calculation is needed.

Example: Discount Calculator

class DiscountCalculator {
    public double calculateDiscount(double price, double percentage) {
        if (price < 0 || percentage < 0 || percentage > 100) {
            throw new IllegalArgumentException("Invalid price or percentage");
        }

        return price * percentage / 100;
    }
}

public class Main {
    public static void main(String[] args) {
        DiscountCalculator calculator = new DiscountCalculator();

        double discount1 = calculator.calculateDiscount(1000, 10);
        double discount2 = calculator.calculateDiscount(2000, 15);

        System.out.println("First discount: " + discount1);
        System.out.println("Second discount: " + discount2);
    }
}

Output

First discount: 100.0
Second discount: 300.0

Explanation

  • DiscountCalculator has one clear responsibility.
  • The calculation logic is written once and reused for different prices.
  • The method validates its inputs before calculating the discount.
  • The class can be used by checkout, billing, or reporting code without duplicating the formula.

4. Characteristics of a Good Reusable Class

  • Single responsibility: The class focuses on one closely related area of functionality.
  • Encapsulation: Internal state is protected from uncontrolled access.
  • Clear public API: Public methods are easy to understand and use.
  • Low coupling: The class depends on as few implementation details of other classes as practical.
  • High cohesion: Its fields and methods contribute to a common purpose.
  • Validation: Invalid inputs are handled consistently.
  • Extensibility: The design allows appropriate changes without requiring widespread modifications.
  • Testability: The class can be tested independently or through well-defined collaborators.

5. Understanding Classes and Objects

A class is a blueprint that defines the data and behavior of objects. An object is a concrete instance of a class, created using the new keyword for ordinary classes.

Example

class Student {
    private String name;
    private int age;

    public Student(String name, int age) {
        this.name = name;
        this.age = age;
    }

    public void displayDetails() {
        System.out.println("Name: " + name);
        System.out.println("Age: " + age);
    }
}

public class Main {
    public static void main(String[] args) {
        Student student1 = new Student("Aarav", 20);
        Student student2 = new Student("Priya", 22);

        student1.displayDetails();
        student2.displayDetails();
    }
}

Output

Name: Aarav
Age: 20
Name: Priya
Age: 22

The Student class defines the structure, while student1 and student2 refer to separate objects with their own state.

Reusability begins when one class definition can create many objects without requiring a separate class for every student.

6. Applying Encapsulation for Reusable Design

Encapsulation combines data and related behavior within a class while controlling how other code accesses its internal state. It commonly uses private fields and public methods.

Without encapsulation, external code may change an object's fields directly and place the object in an invalid state. A reusable class should protect important rules, also called invariants, through its constructors and methods.

Example: Bank Account

class BankAccount {
    private String accountHolder;
    private double balance;

    public BankAccount(String accountHolder, double openingBalance) {
        if (accountHolder == null || accountHolder.isBlank()) {
            throw new IllegalArgumentException("Account holder is required");
        }

        if (openingBalance < 0) {
            throw new IllegalArgumentException("Opening balance cannot be negative");
        }

        this.accountHolder = accountHolder;
        this.balance = openingBalance;
    }

    public String getAccountHolder() {
        return accountHolder;
    }

    public double getBalance() {
        return balance;
    }

    public void deposit(double amount) {
        if (amount <= 0) {
            throw new IllegalArgumentException("Deposit must be positive");
        }

        balance += amount;
    }

    public boolean withdraw(double amount) {
        if (amount <= 0) {
            throw new IllegalArgumentException("Withdrawal must be positive");
        }

        if (amount > balance) {
            return false;
        }

        balance -= amount;
        return true;
    }
}

public class Main {
    public static void main(String[] args) {
        BankAccount account = new BankAccount("Neha", 5000);

        account.deposit(1000);
        account.withdraw(1500);

        System.out.println("Account holder: " + account.getAccountHolder());
        System.out.println("Balance: " + account.getBalance());
    }
}

Output

Account holder: Neha
Balance: 4500.0

Why This Design Is Reusable

  • The balance cannot be modified directly by unrelated classes.
  • Deposit and withdrawal rules are centralized.
  • The same class can represent many bank accounts.
  • The implementation can evolve without requiring client code to manipulate the balance field directly.

7. Abstraction and Clear Public Interfaces

Abstraction means exposing essential operations while hiding unnecessary implementation details. It helps users of a class understand what they can do without needing to know how every operation works internally.

For example, an application can call paymentProcessor.processPayment() without handling every low-level step involved in communicating with a payment provider.

Example: Payment Interface

interface PaymentProcessor {
    boolean processPayment(double amount);
}

class DemoPaymentProcessor implements PaymentProcessor {
    @Override
    public boolean processPayment(double amount) {
        if (amount <= 0) {
            throw new IllegalArgumentException("Amount must be positive");
        }

        System.out.println("Processing payment of Rs. " + amount);
        return true;
    }
}

public class Main {
    public static void main(String[] args) {
        PaymentProcessor processor = new DemoPaymentProcessor();
        processor.processPayment(1200);
    }
}

Output

Processing payment of Rs. 1200.0

The caller depends on the PaymentProcessor contract rather than needing to know the concrete class's internal implementation. The example uses a demonstration processor, not a real payment integration.

8. Inheritance and Code Reuse

Inheritance allows a subclass to extend one superclass using the extends keyword. The subclass can reuse accessible behavior from its parent and add or specialize functionality.

Inheritance is useful when a genuine is-a relationship exists. For example, a Car is a type of Vehicle. It is less appropriate when the relationship is merely that one object uses another.

Example: Vehicle Hierarchy

class Vehicle {
    protected String brand;

    public Vehicle(String brand) {
        this.brand = brand;
    }

    public void start() {
        System.out.println(brand + " vehicle is starting");
    }
}

class Car extends Vehicle {
    public Car(String brand) {
        super(brand);
    }

    public void openTrunk() {
        System.out.println("Car trunk opened");
    }
}

public class Main {
    public static void main(String[] args) {
        Car car = new Car("Toyota");

        car.start();
        car.openTrunk();
    }
}

Output

Toyota vehicle is starting
Car trunk opened

The Car class reuses the constructor and start() behavior from Vehicle and adds its own operation.

When to Use Inheritance

  • There is a clear subtype relationship.
  • The subclass can honor the behavior promised by the superclass.
  • Shared behavior is genuinely common to the parent type.
  • The hierarchy is not being created solely to avoid a small amount of code duplication.

9. Composition: Reuse Through Collaboration

Composition is a design technique in which one object contains a reference to another object and delegates part of its work to that collaborator. It represents a has-a relationship.

Composition often provides more flexibility than inheritance because a class can reuse a collaborator without becoming a subtype of that collaborator's class.

Example: Car and Engine

class Engine {
    public void start() {
        System.out.println("Engine started");
    }
}

class Car {
    private final Engine engine;

    public Car(Engine engine) {
        if (engine == null) {
            throw new IllegalArgumentException("Engine is required");
        }

        this.engine = engine;
    }

    public void startCar() {
        engine.start();
        System.out.println("Car is ready");
    }
}

public class Main {
    public static void main(String[] args) {
        Engine engine = new Engine();
        Car car = new Car(engine);

        car.startCar();
    }
}

Output

Engine started
Car is ready

Explanation

  • The Engine class manages engine-related behavior.
  • The Car class uses an Engine rather than inheriting from it.
  • The engine is supplied through the constructor, making the dependency explicit.
  • The engine implementation can be changed later if the design uses an appropriate abstraction.

Composition is particularly useful for reusable services such as logging, notifications, validation, reporting, and data access.

10. Association, Aggregation, and Composition

These terms describe relationships between objects and help developers decide how classes should collaborate.

Relationship Meaning Example
Association Two objects know about or interact with each other. A teacher teaches a student.
Aggregation A whole references parts that can exist independently. A department has employees who can exist independently of that department.
Composition A whole owns its component as part of its internal structure or lifecycle model. An order contains order-line objects as part of its structure.

The exact lifecycle relationship depends on the application's domain and implementation. These terms are conceptual design tools, not merely different Java keywords.

11. Using Interfaces to Build Flexible Class Structures

Interfaces define contracts that can be implemented by multiple classes. They help reusable designs avoid depending on one specific implementation.

Example: Notification Service

interface NotificationService {
    void send(String recipient, String message);
}

class EmailNotificationService implements NotificationService {
    @Override
    public void send(String recipient, String message) {
        System.out.println("Email to " + recipient + ": " + message);
    }
}

class SmsNotificationService implements NotificationService {
    @Override
    public void send(String recipient, String message) {
        System.out.println("SMS to " + recipient + ": " + message);
    }
}

class AlertManager {
    private final NotificationService notificationService;

    public AlertManager(NotificationService notificationService) {
        this.notificationService = notificationService;
    }

    public void sendAlert(String recipient, String message) {
        notificationService.send(recipient, message);
    }
}

public class Main {
    public static void main(String[] args) {
        NotificationService service = new EmailNotificationService();
        AlertManager manager = new AlertManager(service);

        manager.sendAlert("[email protected]", "Your order is ready");
    }
}

Output

Email to [email protected]: Your order is ready

The AlertManager class depends on the NotificationService interface. To use SMS instead, the application can provide an SmsNotificationService object without changing the core alert logic.

12. Polymorphism and Reusable Methods

Polymorphism allows different objects to be used through the same superclass or interface type. When an overridden instance method is called, the implementation corresponding to the actual object is selected at runtime.

Example: Shape Calculator

interface Shape {
    double calculateArea();
}

class Circle implements Shape {
    private final double radius;

    public Circle(double radius) {
        if (radius <= 0) {
            throw new IllegalArgumentException("Radius must be positive");
        }

        this.radius = radius;
    }

    @Override
    public double calculateArea() {
        return Math.PI * radius * radius;
    }
}

class Rectangle implements Shape {
    private final double length;
    private final double width;

    public Rectangle(double length, double width) {
        if (length <= 0 || width <= 0) {
            throw new IllegalArgumentException("Dimensions must be positive");
        }

        this.length = length;
        this.width = width;
    }

    @Override
    public double calculateArea() {
        return length * width;
    }
}

public class Main {
    static void printArea(Shape shape) {
        System.out.printf("Area: %.2f%n", shape.calculateArea());
    }

    public static void main(String[] args) {
        printArea(new Circle(3));
        printArea(new Rectangle(4, 5));
    }
}

Output

Area: 28.27
Area: 20.00

The printArea() method can work with any suitable Shape implementation. A new shape can be introduced without writing a new version of the method for each shape type.

13. Avoiding Code Duplication

Code duplication occurs when the same business rule or operation is implemented in several places. If the rule changes, developers must find and update every copy, increasing the chance of inconsistent behavior.

Example of Duplicate Logic

class OnlineOrder {
    double calculateTax(double price) {
        return price * 0.18;
    }
}

class StoreOrder {
    double calculateTax(double price) {
        return price * 0.18;
    }
}

Both classes repeat the same tax calculation. If the application needs a different rate, each copy may need to be updated.

Improved Design

class TaxCalculator {
    public double calculateTax(double price, double taxRate) {
        if (price < 0 || taxRate < 0) {
            throw new IllegalArgumentException("Values cannot be negative");
        }

        return price * taxRate;
    }
}

class OnlineOrder {
    private final TaxCalculator taxCalculator;

    public OnlineOrder(TaxCalculator taxCalculator) {
        this.taxCalculator = taxCalculator;
    }

    public double calculateTax(double price, double taxRate) {
        return taxCalculator.calculateTax(price, taxRate);
    }
}

class StoreOrder {
    private final TaxCalculator taxCalculator;

    public StoreOrder(TaxCalculator taxCalculator) {
        this.taxCalculator = taxCalculator;
    }

    public double calculateTax(double price, double taxRate) {
        return taxCalculator.calculateTax(price, taxRate);
    }
}

Both order classes reuse one calculator. In a production application, the tax rate should come from an appropriate business rule or configuration source rather than being arbitrarily duplicated across the application.

14. Single Responsibility Principle

The Single Responsibility Principle (SRP) states that a class should have one cohesive responsibility and one primary reason to change.

For example, a class that calculates an invoice, saves it to a database, sends an email, and formats a PDF has several unrelated responsibilities. Changes to storage or email delivery could affect the same class that contains billing rules.

Less Maintainable Design

class InvoiceManager {
    public void calculateTotal() {
        System.out.println("Calculating invoice total");
    }

    public void saveToDatabase() {
        System.out.println("Saving invoice");
    }

    public void sendEmail() {
        System.out.println("Sending invoice email");
    }
}

Improved Design

class InvoiceCalculator {
    public double calculateTotal(double subtotal, double tax) {
        return subtotal + tax;
    }
}

class InvoiceRepository {
    public void save() {
        System.out.println("Saving invoice");
    }
}

class InvoiceEmailService {
    public void send() {
        System.out.println("Sending invoice email");
    }
}

Each class has a more focused responsibility. The application can change email delivery without changing invoice calculation logic.

15. Open/Closed Principle

The Open/Closed Principle states that software components should be open for extension but closed for modification in the sense that new behavior should often be added through new implementations rather than repeatedly changing stable code.

Example: Discount Strategies

interface DiscountStrategy {
    double applyDiscount(double price);
}

class NoDiscount implements DiscountStrategy {
    @Override
    public double applyDiscount(double price) {
        return price;
    }
}

class TenPercentDiscount implements DiscountStrategy {
    @Override
    public double applyDiscount(double price) {
        return price * 0.90;
    }
}

class Checkout {
    private final DiscountStrategy discountStrategy;

    public Checkout(DiscountStrategy discountStrategy) {
        this.discountStrategy = discountStrategy;
    }

    public double getFinalPrice(double price) {
        if (price < 0) {
            throw new IllegalArgumentException("Price cannot be negative");
        }

        return discountStrategy.applyDiscount(price);
    }
}

public class Main {
    public static void main(String[] args) {
        Checkout checkout = new Checkout(new TenPercentDiscount());
        System.out.println(checkout.getFinalPrice(1000));
    }
}

Output

900.0

A new discount strategy can be added as another implementation of DiscountStrategy. The checkout class does not need a new conditional branch for every new strategy.

16. Dependency Inversion and Dependency Injection

The Dependency Inversion Principle encourages high-level business logic to depend on abstractions instead of concrete implementation details. Dependency injection is a technique for supplying an object's dependencies from outside rather than constructing every dependency internally.

Example

interface MessageSender {
    void sendMessage(String message);
}

class ConsoleMessageSender implements MessageSender {
    @Override
    public void sendMessage(String message) {
        System.out.println(message);
    }
}

class ReminderService {
    private final MessageSender sender;

    public ReminderService(MessageSender sender) {
        this.sender = sender;
    }

    public void remind(String message) {
        sender.sendMessage(message);
    }
}

public class Main {
    public static void main(String[] args) {
        MessageSender sender = new ConsoleMessageSender();
        ReminderService service = new ReminderService(sender);

        service.remind("Meeting starts at 10 AM");
    }
}

Output

Meeting starts at 10 AM

The reminder service does not create its own sender. It receives one through its constructor, making the dependency visible and easier to replace during testing.

17. Prefer Composition Over Inheritance When Appropriate

Composition is often preferable when a class needs to use another object's behavior without representing a subtype of that object. Inheritance is useful for genuine subtype relationships, while composition provides flexible collaboration between independent components.

Aspect Inheritance Composition
Relationship Is-a Has-a or uses-a
Mechanism extends Object references and delegation
Reuse Reuses inherited accessible behavior Reuses behavior through a collaborator
Flexibility Can create strong coupling to a superclass Can allow collaborator implementations to be changed
Example Car extends Vehicle Car contains an Engine

Do not avoid inheritance entirely. Choose the relationship that accurately represents the domain and produces the clearest design.

18. Designing Immutable Classes

An immutable object cannot have its observable state changed after construction. Immutability can simplify reusable class design because callers do not need to worry about unexpected changes to the object's state.

Example: Product

final class Product {
    private final String name;
    private final double price;

    public Product(String name, double price) {
        if (name == null || name.isBlank()) {
            throw new IllegalArgumentException("Product name is required");
        }

        if (price < 0) {
            throw new IllegalArgumentException("Price cannot be negative");
        }

        this.name = name;
        this.price = price;
    }

    public String getName() {
        return name;
    }

    public double getPrice() {
        return price;
    }
}

public class Main {
    public static void main(String[] args) {
        Product product = new Product("Keyboard", 1500);

        System.out.println(product.getName());
        System.out.println(product.getPrice());
    }
}

Output

Keyboard
1500.0

The class is declared final, its fields are private final, and it exposes no mutator methods. Because its fields are immutable values, callers cannot change the product's name or price after construction.

19. Organizing Reusable Classes with Packages

Packages group related Java classes and interfaces into namespaces. They help organize large projects, reduce naming conflicts, and define package-level access boundaries.

Example Package Structure

com.store.model
    Product.java
    Customer.java
    Order.java

com.store.service
    CheckoutService.java
    DiscountService.java

com.store.repository
    ProductRepository.java
    OrderRepository.java

com.store.notification
    NotificationService.java
    EmailNotificationService.java

Declaring a Package

package com.store.model;

public class Product {
    private final String name;
    private final double price;

    public Product(String name, double price) {
        this.name = name;
        this.price = price;
    }

    public String getName() {
        return name;
    }

    public double getPrice() {
        return price;
    }
}

Other classes can import com.store.model.Product and use its public API. In normal Java source organization, the package declaration appears at the beginning of the source file, before import declarations.

20. Designing Class Relationships

Before creating classes, identify the application's main concepts and how they interact. Avoid creating one large class that performs every operation.

For an online shopping system, a possible structure is:

Class or Interface Responsibility
Product Stores product details such as name and price.
Customer Represents a customer and relevant customer information.
ShoppingCart Manages selected products and quantities.
Order Represents a placed order and its line items.
DiscountStrategy Defines the discount calculation contract.
PaymentProcessor Defines payment-processing behavior.
CheckoutService Coordinates the checkout workflow.
OrderRepository Defines how orders are saved and retrieved.
NotificationService Defines customer notification behavior.

This structure separates data models, business logic, external operations, and coordination responsibilities. Each class can be developed and tested more independently.

21. Practical Project: Reusable Shopping Cart System

Project objective: Build a small Java shopping cart that uses reusable classes, encapsulation, composition, and interfaces.

Step 1: Create the Product Class

class Product {
    private final String name;
    private final double price;

    public Product(String name, double price) {
        if (name == null || name.isBlank()) {
            throw new IllegalArgumentException("Name is required");
        }

        if (price < 0) {
            throw new IllegalArgumentException("Price cannot be negative");
        }

        this.name = name;
        this.price = price;
    }

    public String getName() {
        return name;
    }

    public double getPrice() {
        return price;
    }
}

Step 2: Create the ShoppingCart Class

import java.util.ArrayList;
import java.util.List;

class ShoppingCart {
    private final List<Product> products = new ArrayList<>();

    public void addProduct(Product product) {
        if (product == null) {
            throw new IllegalArgumentException("Product is required");
        }

        products.add(product);
    }

    public double calculateTotal() {
        double total = 0;

        for (Product product : products) {
            total += product.getPrice();
        }

        return total;
    }

    public void displayProducts() {
        for (Product product : products) {
            System.out.println(product.getName() + ": Rs. " + product.getPrice());
        }
    }
}

Step 3: Create the Main Class

public class Main {
    public static void main(String[] args) {
        Product keyboard = new Product("Keyboard", 1500);
        Product mouse = new Product("Mouse", 700);

        ShoppingCart cart = new ShoppingCart();
        cart.addProduct(keyboard);
        cart.addProduct(mouse);

        cart.displayProducts();
        System.out.println("Total: Rs. " + cart.calculateTotal());
    }
}

Output

Keyboard: Rs. 1500.0
Mouse: Rs. 700.0
Total: Rs. 2200.0

Project Explanation

  • Product represents an individual product and protects its fields.
  • ShoppingCart stores and manages multiple product objects.
  • The cart uses composition-like collaboration by holding references to product objects.
  • The total calculation is implemented once and can be reused.
  • The classes can be extended with quantities, discounts, checkout, and persistence as new requirements emerge.

This is an educational example. A production shopping cart would also need to consider quantities, currency and rounding rules, product availability, pricing changes, and checkout validation.

22. Unit Testing Reusable Classes

Reusable classes should behave consistently across different callers. Unit tests help verify their behavior independently of the rest of the application.

Example: Testing a Calculator Method

class Calculator {
    public int add(int first, int second) {
        return first + second;
    }
}

A simple test can verify that the method returns the expected value:

public class CalculatorTestExample {
    public static void main(String[] args) {
        Calculator calculator = new Calculator();

        int actual = calculator.add(10, 20);
        int expected = 30;

        if (actual != expected) {
            throw new AssertionError("Expected " + expected + ", got " + actual);
        }

        System.out.println("Test passed");
    }
}

Output

Test passed

This is a basic assertion example rather than a full unit-testing framework. Larger Java projects commonly use JUnit to organize tests, setup, assertions, and test execution.

23. Common Mistakes in Class Design

Mistake Why It Is a Problem Better Approach
Making every field public External code can change state without validation. Keep fields private and expose suitable methods.
Creating one giant class Unrelated responsibilities become difficult to maintain. Separate cohesive responsibilities into different classes.
Using inheritance only to reuse code It may create an incorrect subtype relationship and tight coupling. Consider composition or delegation.
Duplicating business rules Copies can become inconsistent when requirements change. Centralize shared rules in a suitable class or service.
Depending on concrete implementations everywhere Changing implementations requires widespread edits. Use interfaces and dependency injection where appropriate.
Creating too many tiny classes without purpose Unnecessary abstraction can make the code harder to follow. Create classes when they represent a clear concept or responsibility.
Ignoring input validation Objects can enter invalid states. Validate constructor arguments and method inputs.
Using unclear method names Callers cannot easily understand the public API. Use descriptive names that express intent.

24. Best Practices for Reusable Object-Oriented Design

  • Start by identifying the domain concepts and responsibilities.
  • Give each class a clear purpose and cohesive methods.
  • Keep implementation details private unless they must be exposed.
  • Use constructors to establish valid initial state.
  • Prefer immutable fields and objects when practical.
  • Use inheritance only when a genuine subtype relationship exists.
  • Prefer composition when a class needs to collaborate with another object.
  • Define interfaces around meaningful contracts.
  • Inject replaceable dependencies rather than constructing them everywhere.
  • Reduce duplication, but avoid abstractions that do not solve a real problem.
  • Organize related classes into meaningful packages.
  • Write unit tests for important business rules.
  • Document public methods and any important assumptions.
  • Refactor when design problems become evident rather than predicting every possible future requirement.

25. Practice Exercises

Exercise 1: Employee Management

Create an Employee class with private fields for name, employee ID, and salary. Add validation and methods to display employee details.

Exercise 2: Reusable Tax Calculator

Create a TaxCalculator class with a method that accepts a price and tax rate. Validate inputs and test it with multiple values.

Exercise 3: Notification System

Create a NotificationService interface and implement email and SMS notification classes. Pass the implementation into a reusable alert manager.

Exercise 4: Composition

Create a Car class that uses an Engine object. Make the engine a constructor dependency and delegate the startup operation to it.

Exercise 5: Shopping Cart

Extend the shopping cart project to support product quantities, subtotal calculations, and a reusable discount strategy.

Exercise 6: Refactoring

Take a class that calculates totals, saves data, and sends messages. Split it into cohesive classes and explain how each new class improves maintainability.

26. Interview Questions and Answers

Q1. What is a reusable class in Java?

Answer: A reusable class provides a clearly defined responsibility that can be used in multiple contexts without copying its implementation.

Q2. What are the main principles of object-oriented programming?

Answer: The four commonly taught pillars are encapsulation, abstraction, inheritance, and polymorphism.

Q3. What is encapsulation?

Answer: Encapsulation combines data and behavior in a class and controls access to internal state, often using private fields and public methods.

Q4. What is abstraction?

Answer: Abstraction exposes essential behavior while hiding unnecessary implementation details.

Q5. What is the difference between inheritance and composition?

Answer: Inheritance represents an is-a relationship, while composition represents a has-a relationship in which an object uses or contains another object.

Q6. Why is composition often preferred over inheritance?

Answer: Composition can reduce coupling to a superclass and make collaborators easier to replace. Inheritance remains appropriate for genuine subtype relationships.

Q7. What is polymorphism?

Answer: Polymorphism allows different object types to be used through a common superclass or interface, with overridden instance methods selected at runtime.

Q8. What is an interface?

Answer: An interface defines a contract that implementing classes agree to support.

Q9. What is the Single Responsibility Principle?

Answer: It recommends giving a class one cohesive responsibility and one primary reason to change.

Q10. What is the Open/Closed Principle?

Answer: It encourages designs that support adding new behavior through extensions, such as new implementations, without repeatedly modifying stable existing code.

Q11. What is dependency injection?

Answer: Dependency injection supplies an object's dependencies from outside, commonly through a constructor or setter, instead of requiring the object to construct every dependency itself.

Q12. Why should fields often be private?

Answer: Private fields prevent unrelated code from modifying an object's state directly and allow the class to enforce validation rules.

Q13. What is high cohesion?

Answer: High cohesion means that a class's data and methods are closely related to its primary responsibility.

Q14. What is low coupling?

Answer: Low coupling means that classes have fewer unnecessary dependencies on the internal details of other classes.

Q15. How do packages support reusable design?

Answer: Packages organize related classes and interfaces into namespaces, helping manage larger applications and reduce naming conflicts.

27. Quick Revision Checklist

  • A class defines the structure and behavior of its objects.
  • Reusable classes provide clear, cohesive responsibilities.
  • Encapsulation protects internal state.
  • Abstraction exposes essential behavior through a clear API.
  • Inheritance supports reuse through valid subtype relationships.
  • Composition supports reuse through collaboration and delegation.
  • Interfaces define contracts for interchangeable implementations.
  • Polymorphism allows code to work with different implementations through common types.
  • SOLID principles help guide maintainable class design.
  • Packages organize classes into logical groups.
  • Validation helps objects maintain valid state.
  • Unit tests help verify reusable behavior consistently.

28. Conclusion

Designing reusable object-oriented class structures is an essential Core Java skill. Good designs organize data and behavior into cohesive classes, protect internal state, reduce unnecessary dependencies, and reuse functionality through appropriate inheritance, composition, and interfaces.

Reusable code is not simply code that appears in several places. It is code with a clear purpose, a stable and understandable contract, and behavior that can be used safely in different contexts. By practicing encapsulation, polymorphism, dependency injection, and sound class relationships, you can build Java applications that are easier to test, maintain, and extend.


Learn More with JustAcademy

Continue learning Java programming and object-oriented design through the Core Java Training Course.

To explore the course and training options, register for a course demo.

Visit the JustAcademy website for additional courses and learning resources.

Previous Next
whatsapp